SpringAI - ChatMemory
对话记忆
什么是对话记忆
AI记忆
- AI模型本身是无状态的,是不会保留历史交互信息。
- 当需要与AI多轮交互时,如果无法保持上下文(对话记忆),则AI模型可能会出现理解错误的情况。
- 这一特性也算是AI模型的一个局限点,只能应用系统自行实现上下文保持。
- SpringAI主要通过记忆、存储、检索三块来实现与AI保持上下文。
记忆,并非历史记录
- 对话记忆:
- 主要是围绕与AI模型对话过程中用于维持上下文信息,可能只是对话过程中的部分信息。
- 比如最近的多少条对话信息,或者是语意相近的部分对话信息。
- 对话历史记录:则是完整的对话记录,包含用户与AI模型所有的对话信息。
SpringAI实现的记忆
快速使用
摸你一下与AI多轮交互,使用SpringAI提供的
ChatMemory
实现维持上下文构建保留最近5条消息的
ChatMemory
,用于与AI交互过程中实现消息记忆1
2
3
4
5// 聊天记忆
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
// 最大5条聊天记录
.maxMessages(5)
.build();通过SpringAI提供在与AI交互过程中自动从
ChatMemory
添加&检索记忆的Advisor
1
2
3
4
5// 自动从 chatMemory 增加&检索记忆的 Advisor
MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
// 优先级高一些
.order(Ordered.HIGHEST_PRECEDENCE + 10000)
.build();将
Advisor
添加至ChatClient
。这里使用的在构建时默认的Advisor
1
2
3
4ChatClient.builder(chatModel)
// 添加Log Advisor用来输出提示词
.defaultAdvisors(new LogExampleAdvisor(), chatMemoryAdvisor)
.build();模拟一个聊天会话,先创建聊天会话。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19private final ChatClient chatMemoryClient;
private final String system = """
You are an assistant and need to proactively greet users.
The user information is as follows:
```nickname: %s```
""";
public String createChat() {
// 生成一个huihuaID
String cid = UUID.randomUUID().toString();
chatMemoryClient.prompt()
.system(String.format(system, "Jan"))
// 将会话ID添加到 advisor context
.advisors(spec -> spec.params(Map.of(
ChatMemory.CONVERSATION_ID, cid)))
.call()
.content();
return cid;
}模拟与AI聊天。
1
2
3
4
5
6
7
8
9public void chatMemoryExample(String userMsg, String cid) {
chatMemoryClient.prompt()
.user(userMsg)
// 将会话ID添加到 advisor context
.advisors(spec -> spec.params(Map.of(
ChatMemory.CONVERSATION_ID, cid)))
.call()
.content();
}从结果输出上看每次与AI交互的提示词都包含了之前的消息了。(但是体验上不尽人意)
遇见的问题
虽然用上了“记忆”,但是在体验上并不好,AI模型对这些“记忆”消息似乎不太关注。
- 比如先输入“将我接下来输入的内容全部翻译成中文”,AI会回复好的。
- 再输入“Who are you?”,AI当成问题来处理了,而不是直接翻译。
以为是使用上有问题,然后就去
Deepseek
和ChatGpt
平台上使用了一下,发现也是如此。
ChatMemory
ChatMemory
是SpringAI提供的记忆抽象,里面抽象了记忆的基本操作方法,如添加、获取、清理等。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public interface ChatMemory {
/**
* Save the specified message in the chat memory for the specified conversation.
*/
default void add(String conversationId, Message message) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
Assert.notNull(message, "message cannot be null");
this.add(conversationId, List.of(message));
}
/**
* Save the specified messages in the chat memory for the specified conversation.
*/
void add(String conversationId, List<Message> messages);
/**
* Get the messages in the chat memory for the specified conversation.
*/
List<Message> get(String conversationId);
/**
* Clear the chat memory for the specified conversation.
*/
void clear(String conversationId);
}SpringAI只提供了一个
MessageWindowChatMemory
的实现。上面的案例使用的就是这个记忆实现。- 其特性是固定容量(条数)的消息记忆。
- 当消息超出容量限制时,自动移除最早的消息。
MessageWindowChatMemory
的存储默认使用的内存存储。(记忆存储下面会记录)
如果需要其他特性的记忆,就只能开发者自行实现了。比如按时间周期保留记忆。
ChatMemoryRepository
ChatMemoryRepository
是SpringAI抽象的记忆存储接口。以会话ID对聊天记忆进行存储、检索、删除等操作。下面是其源码
InMemoryChatMemoryRepository
InMemoryChatMemoryRepository
是SpringAI默认自动配置的记忆存储。该记忆存储基于
ConcurrentHashMap
实现的内存存储方式。在
MessageWindowChatMemory
中默认使用的也是该存储方式。
JdbcChatMemoryRepository
JdbcChatMemoryRepository
是SpringAI提供的关系型数据库记忆存储。需要额外引入
starter
1
2
3
4<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>JdbcChatMemoryRepository
提供了三个配置属性 说明 默认值 spring.ai.chat.memory.repository.jdbc.initialize-schema
控制初始化 Schema 的时机。可选值: embedded
、always
、never
。embedded
spring.ai.chat.memory.repository.jdbc.schema
用于初始化的 Schema 脚本位置。支持 classpath:
URL 及平台占位符。classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-@@platform@@.sql
spring.ai.chat.memory.repository.jdbc.platform
用于替换始化脚本中 @@platform@@ 占位符,指定平台标识。默认通过 DataSource
自动识别。auto-detected JdbcChatMemoryRepository
的工作流程创建
JdbcChatMemoryRepositorySchemaInitializer
进行脚本初始化,创建SPRING_AI_CHAT_MEMORY
表。以
Mysql
为例,下面是Mysql
建表的脚本源码1
2
3
4
5
6
7
8CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
`conversation_id` VARCHAR(36) NOT NULL,
`content` TEXT NOT NULL,
`type` ENUM('USER', 'ASSISTANT', 'SYSTEM', 'TOOL') NOT NULL,
`timestamp` TIMESTAMP NOT NULL,
INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`)
);在脚本初始化过程中会根据上面的三个配置选择初始化时机、脚本文件、以及数据库平台。
根据
DataSource
创建方言JdbcChatMemoryRepositoryDialect
,用于获取操作数据库表的Sql。构建
JdbcChatMemoryRepository
,在进行记忆操作时是通过方言Dialect
获取Sql并使用Jdbc
执行。
自行模拟一个
JdbcChatMemoryRepository
需求:
- 自行定义一个字段更丰富的存储表。(还是基于
Mysql
) - 不使用脚本初始化过程。
- 自行定义方言
Dialect
提供操作存储表的Sql。 - 自定义
JdbcChatMemoryRepository
- 自行定义一个字段更丰富的存储表。(还是基于
实现:
定义存储表结构
1
2
3
4
5
6
7
8
9
10
11
12CREATE TABLE `AI_CHAT_MEMORY` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`conversation_id` bigint(20) NOT NULL COMMENT '会话ID',
`message` TEXT NOT NULL COMMENT '消息内容',
`message_type` ENUM('USER', 'ASSISTANT', 'SYSTEM', 'TOOL') NOT NULL COMMENT '消息类型',
`sort` TIMESTAMP NOT NULL COMMENT '排序',
`is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除,0否1是',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_conversation_id` (`conversation_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='AI聊天记忆表';配置。手动创建表,关闭脚本初始化过程。
1
spring.ai.chat.memory.repository.jdbc.initialize-schema=never
自行定义方言
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class AIChatMemoryRepositoryDialect implements JdbcChatMemoryRepositoryDialect {
public String getSelectMessagesSql() {
return """
SELECT
message, message_type
FROM
AI_CHAT_MEMORY
WHERE
conversation_id = ?
ORDER BY
sort
""";
}
public String getInsertMessageSql() {
return """
INSERT INTO
AI_CHAT_MEMORY (conversation_id, message, message_type, sort)
VALUES (?, ?, ?, ?)
""";
}
public String getSelectConversationIdsSql() {
// 没什么意义,暂不实现
return "";
}
public String getDeleteMessagesSql() {
// 用逻辑删除
return "UPDATE AI_CHAT_MEMORY SET is_deleted = 1 WHERE conversation_id = ?";
}
}配置
JdbcChatMemoryRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public JdbcChatMemoryRepository jdbcChatMemoryRepository(JdbcTemplate jdbcTemplate) {
return JdbcChatMemoryRepository.builder()
.jdbcTemplate(jdbcTemplate)
// 使用自定义的方言
.dialect(new AIChatMemoryRepositoryDialect())
.build();
}
public ChatClient jdbcChatMemoryClient(DeepSeekChatModel chatModel,
JdbcChatMemoryRepository jdbcChatMemoryRepository) {
// 聊天记忆
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
// 最大5条聊天记录
.maxMessages(5)
// 使用JDBC存储
.chatMemoryRepository(jdbcChatMemoryRepository)
.build();
// 自动从 chatMemory 增加&检索记忆的 Advisor
MessageChatMemoryAdvisor chatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory)
// 优先级高一些
.order(Ordered.HIGHEST_PRECEDENCE + 10000)
.build();
return ChatClient.builder(chatModel)
// 添加Log Advisor用来输出提示词
.defaultAdvisors(new LogExampleAdvisor(), chatMemoryAdvisor)
.build();
}执行案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class JdbcChatMemoryExample {
private final ChatClient jdbcChatMemoryClient;
public String createChat() {
// 生成会话ID
return System.currentTimeMillis() + "";
}
public void chatMemoryExample(String userMsg, String cid) {
jdbcChatMemoryClient.prompt()
.user(userMsg)
.advisors(spec -> spec.params(Map.of(
ChatMemory.CONVERSATION_ID, cid)))
.call()
.content();
}
}案例运行结果
其他记忆存储
除了上面两种记忆存储,SpringAI还提供了NoSql数据库存储以及图数据库存储的实现,这里先暂时简单了解一下,不做深入学习。
CassandraChatMemoryRepository
基于
Apache Cassandra
实现的记忆存储需要添加以下依赖
1
2
3
4<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-cassandra</artifactId>
</dependency>使用方式与
JdbcChatMemoryRepository
一致。
Neo4jChatMemoryRepository
基于图数据库实现的记忆存储
需要添加以下依赖
1
2
3
4<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-neo4j</artifactId>
</dependency>
对于这两种记忆存储,以后机会再进一步深入学习。
ChatMemoryAdvisor
ChatMemoryAdvisor
是SpringAI提供的在与模型交互过程中自动添加/检索记忆的Advisor
。在上面的几个实用案例中都是使用的
MessageChatMemoryAdvisor
来自动管理记忆。SpringAI提供三种不同的
ChatMemoryAdvisor
,如下MessageChatMemoryAdvisor
:指定ChatMemory
来管理会话记忆。在与模型交互时从记忆库检索历史记忆,将其作为Message
集合注入到Prompt
。PromptChatMemoryAdvisor
:也是指定ChatMemory
来管理会话记忆。不同的是将历史记忆以纯文本形式追加至系统(SystemMessage)提示词中,这里是可以自定义模版来追加。(用法与MessageChatMemoryAdvisor
一致的)VectorStoreChatMemoryAdvisor
:指定VectorStore
实现管理会话记忆。通过从向量存储检索历史记忆,以纯文本形式追加至系统(SystemMessage)消息。(后续学习了向量数据库后再进一步学习向量记忆,理论上有效减少记忆增加的Tokens
)
总结
- AI模型是无状态的,不具备对话记忆的能力;记忆是对话过程中用于维持上下文,非所有的历史记录。
- SpringAI主要是通过
ChatMemory
和ChatMemoryAdvisor
来管理记忆。 - SpringAI提供了多种记忆存储,默认是内存存储,其他扩展的如
JDBC
、图数据库等存储。
最后
- 一个成熟的AI应用,其记忆存储肯定是一个较为复杂的解决方案。所以这里只是简单了解了SpringAI是如何来实现对话记忆的。
- 对于SpringAI的基础使用我认为学习到这里算是结束了,到这里可以使用SpringAI快速的开发简单的AI应用,减少一些基础封装的过程。
- 接下来将学习
MCP
/RAG
/向量等进一步提升AI应用能力的技术使用。 - 所有案例的源码,都会提交在GitHub上。本次案例包:
com.spring.ai.example.memory